/*
 * Copyright (C) 2017-2019 Huawei Technologies Duesseldorf GmbH
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2019.
 *
 * Author: Roberto Sassu <roberto.sassu@huawei.com>
 * and adapt to Huawei RTOS by Qixiao Liu <liuqixiao@huawei.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 * File: ima_digest_list.c
 *      Functions to manage digest lists.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <crypto/hash_info.h>
#include <linux/vmalloc.h>
#include <linux/module.h>

#include "ima.h"
#include "rtos_ima_digest_list.h"


struct ima_h_table ima_digests_htable = {
	.len = ATOMIC_LONG_INIT(0),
	.queue[0 ... IMA_MEASURE_HTABLE_SIZE - 1] = HLIST_HEAD_INIT
};

static bool ima_digest_list_data_opened;
static bool ima_digest_list_data_stat = true;
/* ima_seg keeps the trunk of data that is left from current slice */
static char *ima_seg;
static size_t ima_seg_len;
static bool ima_seg_used;
static struct compact_list_hdr *ima_cl_hdr;
static unsigned int ima_d_count;


/* Get/add functions */
struct ima_digest *ima_lookup_digest(u8 *digest, enum hash_algo algo)
{
	struct ima_digest *d = NULL;
	int digest_len = hash_digest_size[algo];
	unsigned int key = ima_hash_key(digest);

	rcu_read_lock();
	hlist_for_each_entry_rcu(d, &ima_digests_htable.queue[key], hnext)
		if (d->algo == algo && !memcmp(d->digest, digest, digest_len))
			break;

	rcu_read_unlock();
	return d;
}

static int ima_add_digest_data_entry(u8 *digest)
{
	struct ima_digest *d = NULL;
	enum hash_algo algo = ima_cl_hdr->algo;
	enum compact_types type = ima_cl_hdr->type;
	u16 modifiers = ima_cl_hdr->modifiers;
	int digest_len = hash_digest_size[algo];
	unsigned int key = ima_hash_key(digest);
#ifdef CONFIG_RTOS_CIV_DIGEST_LIST
	u64 ima_digest_list_count;
#endif

	d = ima_lookup_digest(digest, algo);
	if (d) {
		d->modifiers |= modifiers;
		return -EEXIST;
	}

	d = kmalloc(sizeof(*d) + digest_len, GFP_KERNEL);
	if (d == NULL)
		return -ENOMEM;

	d->algo = algo;
	d->type = type;
	d->modifiers = modifiers;

	memcpy(d->digest, digest, digest_len);
#ifdef CONFIG_RTOS_CIV_DIGEST_LIST
	ima_digest_list_count = atomic_long_read(&ima_digests_htable.len) + 1;
	if (ima_digest_list_count_max && (ima_digest_list_count >= ima_digest_list_count_max)) {
		kfree(d);
		d = NULL;
		pr_err_once("Current count of digest_list_data reaches digest_list_max\n");
		return -EINVAL;
	}
#endif
	hlist_add_head_rcu(&d->hnext, &ima_digests_htable.queue[key]);
	atomic_long_inc(&ima_digests_htable.len);
	return 0;
}

static void ima_seg_free(void)
{
	kfree(ima_seg);
	ima_seg = NULL;
	ima_seg_len = 0;
	ima_seg_used = false;
}

static int ima_seg_init(size_t size)
{
	size_t digest_len;

	if (ima_seg != NULL)
		ima_seg_free();

	if (ima_cl_hdr != NULL)
		digest_len = hash_digest_size[ima_cl_hdr->algo];
	else
		digest_len = size;

	ima_seg = kmalloc(digest_len, GFP_KERNEL);
	if (ima_seg == NULL)
		return -ENOMEM;

	ima_seg_len = size;
	return 0;
}

static void ima_cl_hdr_free(void)
{
	if (ima_cl_hdr != NULL) {
		kfree(ima_cl_hdr);
		ima_cl_hdr = NULL;
	}
}

static int ima_compact_list_hdr_verify(void)
{
	size_t digest_len;

	if (ima_cl_hdr->version != 1)
		goto out_free;
	if (ima_cl_hdr->algo >= HASH_ALGO__LAST)
		goto out_free;
	if (ima_cl_hdr->type >= COMPACT__LAST)
		goto out_free;
	digest_len = hash_digest_size[ima_cl_hdr->algo];
	if ((ima_cl_hdr->count * digest_len) != ima_cl_hdr->datalen)
		goto out_free;

	return 0;
out_free:
	ima_cl_hdr_free();
	pr_err("compact list hdr invalid format\n");
	return -EINVAL;
}

/* Compact list parser */
static int ima_set_compact_list_hdr(const char *buf, size_t size)
{
	size_t hdr_size = sizeof(struct compact_list_hdr);
	u8 *tmp = NULL;
	int ret;

	if (hdr_size > size) {
		if (ima_seg_init(size) < 0)
			return -ENOMEM;
		memcpy(ima_seg, buf, size);
		ima_seg_used = true;
		return (int)size;
	}
	if (ima_seg_used == true) {
		tmp = kmalloc(hdr_size, GFP_KERNEL);
		if (tmp == NULL)
			return -ENOMEM;
		memcpy(tmp, ima_seg, ima_seg_len);
		if (size < hdr_size - ima_seg_len) {
			kfree(tmp);
			tmp = NULL;
			return -EINVAL;
		}
		memcpy(tmp + ima_seg_len, buf, hdr_size - ima_seg_len);
		ima_cl_hdr = (struct compact_list_hdr *) tmp;
		hdr_size = hdr_size - ima_seg_len;
		ima_seg_free();
	} else {
		ima_cl_hdr = kmalloc(hdr_size, GFP_KERNEL);
		if (ima_cl_hdr == NULL)
			return -ENOMEM;
		memcpy(ima_cl_hdr, buf, hdr_size);
	}
	if (ima_canonical_fmt) {
		ima_cl_hdr->type = le16_to_cpu(ima_cl_hdr->type);
		ima_cl_hdr->modifiers = le16_to_cpu(ima_cl_hdr->modifiers);
		ima_cl_hdr->algo = le16_to_cpu(ima_cl_hdr->algo);
		ima_cl_hdr->count = le32_to_cpu(ima_cl_hdr->count);
		ima_cl_hdr->datalen = le32_to_cpu(ima_cl_hdr->datalen);
	}
	ret = ima_compact_list_hdr_verify();
	if (ret < 0)
		return ret;

	ima_d_count = 0;
	return (int)hdr_size;
}

int ima_parse_compact_list(loff_t size, char *buf)
{
	char *digest = NULL;
	char *bufp = NULL;
	char *bufendp = NULL;
	size_t digest_len;
	int ret;

	bufp = buf;
	bufendp = buf + size;
	while (bufp < bufendp) {
		if (ima_cl_hdr == NULL) {
			ret = ima_set_compact_list_hdr(bufp, bufendp - bufp);
			if (ret < 0)
				return ret;
			bufp += ret;
			continue;
		}

		digest_len = hash_digest_size[ima_cl_hdr->algo];
		if (bufp + digest_len > bufendp) {
			if (ima_seg_len != bufendp - bufp && ima_seg_init(bufendp - bufp) < 0)
				return -ENOMEM;
			memcpy(ima_seg, bufp, ima_seg_len);
			ima_seg_used = true;
			return bufendp - buf;
		}
		if (bufp == buf && ima_seg_used == true) {
			digest = ima_seg;
			ima_seg_used = false;
			memcpy(digest + ima_seg_len, bufp, digest_len -
					ima_seg_len);
			bufp += digest_len - ima_seg_len;
		} else {
			digest = bufp;
			bufp += digest_len;
		}

		ret = ima_add_digest_data_entry(digest);
		if (ret < 0 && ret != -EEXIST)
			return ret;
		ima_d_count++;
		if (ima_d_count == ima_cl_hdr->count) {
			ima_cl_hdr_free();
			ima_d_count = 0;
		}
	}
	return bufp - buf;
}

/* digest_list_data file status */
void ima_set_digest_list_data_stat(bool value)
{
	ima_digest_list_data_stat = value;
}

bool ima_get_digest_list_data_stat(void)
{
	return ima_digest_list_data_stat;
}

/* digest_list_data upload stat clean */
bool ima_digest_list_upload_stat(void)
{
	bool result = true;

	if (ima_seg_used == true)
		result = false;
	if (ima_d_count > 0)
		result = false;

	if (!result)
		pr_err("compact list is not aligned\n");

	if (ima_seg != NULL)
		ima_seg_free();
	ima_cl_hdr_free();
	ima_d_count = 0;
	return result;
}

/* Securityfs status */
void ima_set_digest_list_data_opened(bool value)
{
	ima_digest_list_data_opened = value;
}

bool ima_get_digest_list_data_opened(void)
{
	return ima_digest_list_data_opened;
}
